2021.6.26 星期六 11:47
ORM 介绍
ORM(Object Relational Mapping,对象关系映射),是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术,通过描述对象和数据库之间映射的元数据,把程序中的对象自动持久化到关系数据库中。
它的作用是在关系型数据库和对象之间作一个映射,这样,我们在具体的操作数据库的时候,就不需要再去和复杂的SQL语句打交道,只要像平时操作对象一样操作它就可以了 。
ORM就是把业务实体中的对象与关系数据库中的关系数据关联起来。
对象-关系映射(ORM)系统一般以中间件的形式存在,主要实现程序对象到关系数据库数据的映射。
ORM技术特点:
- 提高了开发效率。ORM可以自动对Entity对象与数据库中的Table进行字段与属性的映射,所以我们实际可能已经不需要一个专用的、庞大的数据访问层。
- ORM提供了对数据库的映射,不用sql直接编码,能够像操作对象一样从数据库获取数据。
ORM的优缺点:
- 性能影响。面向对象的处理方式会对性能造成影响
- 更多的系统层次会造成执行效率的降低。
常见框架
常见的ORM框架
Java系列:
Apache OJB
Hibernate:目前最流行的开源ORM框架
iBatic
Mybatis
.Net系列:
NHibernate:面向.NET环境的对象/关系数据库映射工具
Linq to sql:适用于一些轻型的,小的ORM适用
EntitysCodeGenerate
PetaPoco
Node.js系列:
ORM2:https://github.com/dresende/node-orm2
sequelize:本文要研究的框架,较常用
Knex.js:官网:https://knexjs.org/
TypeORM:采用 TypeScript 编写,支持使用 TypeScript 或 Javascript(ES5,ES6,ES7) 开发。目标是保持支持最新的 Javascript 特性来帮助开发各种用户数据库的应用 - 不管是轻应用还是企业级的
简单使用
ORM的两种模式
Active Record 模式:活动记录模式,领域模型模式一个模型类对应关系型数据库中的一个表,模型类的一个实例对应表中的一行记录。这个不难理解,比较简单,但是不够灵活,再看另一种模式,比较一下
Data Mapper 模式:数据映射模式,领域模型对象和数据表是松耦合关系,只进行业务逻辑的处理,和数据层解耦。需要一个实体管理器来将模型和持久化层做对应,这样一来,灵活性就高,当然复杂性也增加了。
所以说,Data Mapper模式对业务代码干预少,Active Record模式直接在对象上CRUD,代码编写也更方便,这就像hibernate和mybatis两种框架,如果想深入研究,可以了解一下贫血与充血领域对象的平衡。
有这么一句话很认同,ActiveRecord更加适合快速开发成型的短期简单项目,而DataMapper更加适合长线开发,保持业务逻辑与数据存储独立的复杂项目。除此之外,技术选型还要考虑其他因素,比如项目历史背景等等。
TypeORM
TypeORM 是一个 ORM 框架,详细介绍见 TypeORM 官方介绍,TypeORM 也借鉴了hibernate,所以你会发现它特别熟悉,尤其是装饰类的方式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34// 实体model,user类
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
age: number;
}
// CRUD操作:逻辑层
import "reflect-metadata";
import {createConnection} from "typeorm";
import {User} from "./entity/User";
createConnection().then(async connection => {
console.log("Inserting a new user into the database...");
const user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.age = 25;
await connection.manager.save(user);
console.log("Saved a new user with id: " + user.id);
console.log("Loading users from the database...");
const users = await connection.manager.find(User);
console.log("Loaded users: ", users);
console.log("Here you can setup and run express/koa/any other framework.");
}).catch(error => console.log(error));
Sequelize
这个被star数最多了一个ORM框架,官方居然不给中文文档,找个CLI命令快速构建也没有,也没找到个合适轮子,只能自己搭了,也不是少了轮子就不能活了。不过Sequelize的官网文档看着很顺眼,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129// # 构建数据库访问公共文件db.js
const Sequelize = require('sequelize')
const {
dbName,
host,
port,
user,
password
} = require('../config').database
const sequelize = new Sequelize(dbName, user, password, {
dialect: 'mysql',
host,
port,
logging: true,
timezone: '+08:00',
dialect: 'mysql'|'sqlite'|'postgres'|'mssql',
pool: {
max: 5,
min: 0,
idle: 10000
},
// SQLite only
storage: 'path/to/database.sqlite',
// operatorsAliases, // 定义别名。(可能不需要??)
define: {
// create_time && update_time
timestamps: true,
// delete_time
paranoid: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
deletedAt: 'deleted_at',
// 把驼峰命名转换为下划线
underscored: true,
scopes: {
bh: {
attributes: {
exclude: ['password', 'updated_at', 'deleted_at', 'created_at']
}
},
iv: {
attributes: {
exclude: ['content', 'password', 'updated_at', 'deleted_at']
}
}
}
}
})
// 创建模型
sequelize.sync({
force: false
})
module.exports = {
sequelize
}
// # model
const {Sequelize, Model} = require('sequelize')
const {db} = require('../../db')
// ## //定义数据模型
class User extends Model {}
User.init({
// attributes
firstName: {
type: Sequelize.STRING,
allowNull: false
},
lastName: {
type: Sequelize.STRING
// allowNull defaults to true
}
}, {
db,
modelName: 'user'
// options
});
// ### 还有一种写法,兼容老版本,不推荐
const User = db.define('user', {
// attributes
firstName: {
type: Sequelize.STRING,
allowNull: false
},
lastName: {
type: Sequelize.STRING
// allowNull defaults to true
},
birthday: Sequelize.DATE
}, {
// options
});
// ## 同步数据库
// Note: using `force: true` will drop the table if it already exists
User.sync({ force: true }).then(() => {
// Now the `users` table in the database corresponds to the model definition
return User.create({
firstName: 'John',
lastName: 'Hancock'
});
});
// # CRUD操作:然后看一下逻辑层,就非常简单了,直接使用ES7 async/await即可
// Find all users
User.findAll().then(users => {
console.log("All users:", JSON.stringify(users, null, 4));
});
// Create a new user
User.create({ firstName: "Jane", lastName: "Doe" }).then(jane => {
console.log("Jane's auto-generated ID:", jane.id);
});
// Delete everyone named "Jane"
User.destroy({
where: {
firstName: "Jane"
}
}).then(() => {
console.log("Done");
});
// Change everyone without a last name to "Doe"
User.update({ lastName: "Doe" }, {
where: {
lastName: null
}
}).then(() => {
console.log("Done");
});
这种实际上是sequelize.define内部调用了model.init,但是老版本是没有第一种写法的。
此外需要知道的是,sequelize还默认为每个模型定义字段id(主键)、createdat和updatedat,也可以进行设置。
我们的db.js文件里面配置了,不自动创建模型,也就是自动创建数据表,关闭是有原因的,因为如果表存在会先drop然后再创建,这种操作本身就很可怕的
单个模型也可以配置,切记这种操作很危险,尤其是生成环境
由此来看,没有typeorm装饰类的方式看着顺眼,但是整体构造也容易上手,操作简单,容易理解,看官网文档,功能覆盖强大,typeorm用户反馈使用问题比Sequelize要多,后期用到再做比较。
ORM2
ORM2貌似没有正了八经的官网,所以看起来就特别麻烦,但是可以看一下github介绍node-orm2,只支持四种数据库MySQL、PostgreSQL、Amazon Redshift、SQLite,这个我没写demo,直接分析一下
所以,准确应该是node-orm2,写法和sequelize类似,但是文档确实不行,数据库支持也少,很难想象后续的可维护性。
1 | // # 数据库连接 |
## 其它
8 bookshelf(这个用的也挺多)
persistencejs
6 waterline
5 mongoose
node-mysql
knex
汇总对比
12 Best Node.js ORMs
1 RxDB
4 Loopback
7 CaminteJS
9 Objection.js
11 Node-ORM2
12 Mikro-ORM
Node.js ORM 框架对比
### Mongoose
目前比较常见的 MongoDB ORM 框架,官方说法是 ODM 框架,可见对关系型数据库支持一般
官网:https://mongoosejs.com/
数据库:仅支持 MongoDB
编程风格:
支持 Promise/async/await
基于 JS 内置类型的 Schema 声明
基于链式构造的 Query Builder 查询
周边技术:Typegoose
https://www.npmjs.com/package/typegoose
可以增加 TypeScript 支持,支持使用 Reflect Metadata 自动映射 TS 类型标注
热度:周频持续更新,NPM 周下载 70W+
### Sequelize
较老牌的 Node.js ORM 框架,相对简易
官网:http://docs.sequelizejs.com/
数据库:支持关系型数据库(MySQL/MSSQL/PostgreSQL/SQLite)
编程风格:
支持 Promise/async/await
基于自带的一套类型枚举声明
基于 JSON 对象的查询方式
基于自带的一套操作符描述
热度:月频持续更新,NPM 周下载 20W+
### Bookshelf
Sequelize 之后出现的 ORM 框架,风格与 Sequelize 较相似,看上去比 Sequelize 易用性高
官网:http://bookshelfjs.org/
数据库:支持关系型数据库
编程风格:
基本上是 Eloquent ORM 的 JS 版本
支持 Promise/async/await
支持基于链式构造的 Query Builder 查询
热度:近半年未更新,NPM 周下载 1.7W
### TypeORM
基于 Decorator 的 ORM 框架,对 TypeScript 支持较好,同时支持在 JavaScript 中通过手动声明使用,以及 JSON 方式的 Entity 配置声明
官网:https://github.com/typeorm/typeorm/
数据库:支持关系型数据库,Beta 支持 MongoDB
编程风格:
基本上是 Hibernate 的 JS 版本
支持 Promise/async/await
支持基于链式构造的 Query Builder 查询
支持 CLI 工具
热度:周频持续更新,NPM 周下载 2.8W
优化数据库查询 (Sequelize)
2 基础用法
你也可以通过 define 的第三个参数做一些自定义,这些配置会被合并到 Sequlize 构造函数的 define 字段中,用来定义模型和数据表的关联行为,比如「自动更新表中的 update_at、create_at」。
基础的CURD
Sequlize 对象提供丰富的 api,诸如:
findOne、findAll……
create、upsert……
aggregate、max……
一个例子:findAll
第二个例子:findOrCreate
有些高级 API 会::触发数据库事务::
1 | Station.findOrCreate({ |
3 联表查询
3.2 联表关系
在 Sequlize 中,联表关系需要在模型 associate 方法中标记,通常为这种格式:1
2
3
4
5
6
7
8
9
10
11File.belongsTo(User, {...option});
File.findOne({
include: [{ model: User }],
});
// ### 3.3
User.HasOne(File, {
foreignKey: 'creator_id', // 如果不定义这个,也会自动定义为「源模型名 + 源模型主键名」即 user_id
sourceKey: 'id', // 源模型的关联键,默认主键,通常省略
}
两种模型
源模型:需要标记和其他模型关系的模型,就是执行联表查询的模型 (上面的 File)
目标模型:被标记关系的模型,本身不因此次标记获得联表查询能力 (上面的 User)
四种关联键
foreignKey:外键,用来关联外部模型,::一个模型有了外键,对关联的模型来说就是唯一了::
targetKey
sourceKey
otherKey:当一个 foreignKey 不够用时的替代品
表之间的关系通常包括:一对一、一对多、多对多。
3.3 一对一关系(belongsTo / hasOne)
3.4 一对多(hasMany)
3.5 多对多关系(belongsToMany)
3.6 几种 JOIN
4 数据库查询的优化
4.1 慢查询、全表扫描和索引
在数据库界,人们常常提到「慢查询」,指的是查询时长超过指定时长的查询。慢查询的危害在于不仅本次查询的请求时间变长,还会较长时间的占用系统资源,对其他查询造成影响或者干脆撑挂数据库。
而「慢查询」最常见的罪魁祸首就是「全表扫描」,指的是数据库引擎为了找到某条记录,对全表进行逐个搜索,直到搜索到这条记录。
举个例子,当你用主键查一条记录的时候,就不会全表扫描。File.findByPk(123);
因为 MySQL 默认给主键列加了「索引」。
::「索引」厉害在哪?MySQL 为这一列建立了一个 btree::(不同数据库的实现是不一样的,但 btree 是主流)。这样查“id 为 318 的 Station”只需要从根节点沿着找下去,类似这个意思:
3xx –> 31x –> 318
4.2 给其他列加索引
那么如果我查普通列呢?也可以通过索引提升查询效率。
如果嫌这个路径长,还有更近一步的,对于常查的列,比如 File 的 name 和 author,可以建立「覆盖索引」:
create index index_name_and_address on file(name, author);
这时候如果我只根据 name 查 author:
File.findOne({
where: {
name: ‘station1’
},
attributes: [‘author’]
})
因为索引里已经存了 address,就不需要再去访问源数据了:
开始
–> name: sta… –> name: statio –> name: station1
–> 拿到 station1 的 address: xxx
索引越多越好吗?
然而索引并不是越多越好,索引虽然提升了查询的效率,缺牺牲了插入、删除的效率。想象下以前只要把新数据堆到表上就行,现在还要修改索引,更麻烦的是索引是个平衡树,很多场景需要对整个树进行调整。(主键为什么默认是自增的?我猜也是为了减少插入数据时树操作的成本)
所以我们一般考虑在常用来「where」或者「order」的列上加索引。
4.3 查询语句优化
前面说的给常用列增加索引可以提升查询效率,让查询尽量走「btree」而不是「全表扫描」。 但前提是别上来就 select *,而是要用 attributes 只摘取你要的列:1
2
3where: {
attributes: ['id', 'name']
}
但并不是所有的查询都会走「btree」,不优秀的 sql 仍然会触发全表扫描,产生慢查询,应该尽量避免。
当你 where 一个列时,MySQL只有对以下操作符才使用索引:<,<=,=,>,>=,BETWEEN,IN,以及某些时候的LIKE。
放到 Sequelize 里就是:Sequelize.Op.gt|gte|lt|lte|eq|between|in
…
比如,能用 in 尽量别用 not in1
2
3
4
5
6
7
8// 不好
status: {
[Op.notIn]: [ 3, 4, 5, 6 ],
},
// 好
status: {
[Op.in]: [ 1, 2 ],
},
实现ORM框架
使用TypeScript以及mysql包。
mysql.ts文件,主要实现连接mysql,以及一下基本的增删改查函数
ORM.ts,封装ORM类
sequelize
使用sequelize使用三段式:连接数据库
定义模型
同步数据库
Sequelize有哪些特色?
1) 强大的模型定义,支持虚拟类型。Javascript虽然被很多人诟病杂乱无章法,但是函数即对象这个特色,可以说是我的最爱,非常灵活强大。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var Foo = sequelize.define('foo', {
firstname: Sequelize.STRING,
lastname: Sequelize.STRING
}, {
getterMethods : {
fullName : function() { return this.firstname + ' ' + this.lastname }
},
setterMethods : {
fullName : function(value) {
var names = value.split(' ');
this.setDataValue('firstname', names.slice(0, -1).join(' '));
this.setDataValue('lastname', names.slice(-1).join(' '));
},
}
});
2) 支持完善的数据验证,减轻前后端的验证压力。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46var ValidateMe = sequelize.define('foo', {
foo: {
type: Sequelize.STRING,
validate: {
is: ["^[a-z]+$",'i'], // 全匹配字母
is: /^[a-z]+$/i, // 全匹配字母,用规则表达式写法
not: ["[a-z]",'i'], // 不能包含字母
isEmail: true, // 检查邮件格式
isUrl: true, // 是否是合法网址
isIP: true, // 是否是合法IP地址
isIPv4: true, // 是否是合法IPv4地址
isIPv6: true, // 是否是合法IPv6地址
isAlpha: true, // 是否是字母
isAlphanumeric: true, // 是否是数字和字母
isNumeric: true, // 只允许数字
isInt: true, // 只允许整数
isFloat: true, // 是否是浮点数
isDecimal: true, // 是否是十进制书
isLowercase: true, // 是否是小写
isUppercase: true, // 是否大写
notNull: true, // 不允许为null
isNull: true, // 是否是null
notEmpty: true, // 不允许为空
equals: 'specific value', // 等于某些值
contains: 'foo', // 包含某些字符
notIn: [['foo', 'bar']], // 不在列表中
isIn: [['foo', 'bar']], // 在列表中
notContains: 'bar', // 不包含
len: [2,10], // 长度范围
isUUID: 4, // 是否是合法 uuids
isDate: true, // 是否是有效日期
isAfter: "2011-11-05", // 是否晚于某个日期
isBefore: "2011-11-05", // 是否早于某个日期
max: 23, // 最大值
min: 23, // 最小值
isArray: true, // 是否是数组
isCreditCard: true, // 是否是有效信用卡号
// 自定义规则
isEven: function(value) {
if(parseInt(value) % 2 != 0) {
throw new Error('请输入偶数!')
}
}
}
}
});
3) Sequelize的查询非常全面和灵活1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102Project.findAll({
//复杂过滤,可嵌套
where: {
id: {
$and: {a: 5} // AND (a = 5)
$or: [{a: 5}, {a: 6}] // (a = 5 OR a = 6)
$gt: 6, // id > 6
$gte: 6, // id >= 6
$lt: 10, // id < 10
$lte: 10, // id <= 10
$ne: 20, // id != 20
$between: [6, 10], // BETWEEN 6 AND 10
$notBetween: [11, 15], // NOT BETWEEN 11 AND 15
$in: [1, 2], // IN [1, 2]
$notIn: [1, 2], // NOT IN [1, 2]
$like: '%hat', // LIKE '%hat'
$notLike: '%hat' // NOT LIKE '%hat'
$iLike: '%hat' // ILIKE '%hat' (case insensitive) (PG only)
$notILike: '%hat' // NOT ILIKE '%hat' (PG only)
$overlap: [1, 2] // && [1, 2] (PG array overlap operator)
$contains: [1, 2] // @> [1, 2] (PG array contains operator)
$contained: [1, 2] // <@ [1, 2] (PG array contained by operator)
$any: [2,3] // ANY ARRAY[2, 3]::INTEGER (PG only)
},
status: {
$not: false, // status NOT FALSE
}
}
})
Project.all()
findByPk
Project.findById
Project.findByOne
Project.findOrCreate
Project.findAndCountAll
Project.count()
Project.max()
//CRUD
Project.create()
Project.save()
Project.update()
Project.destroy()
//批量
User.bulkCreate([])
//排序
something.findOne({
order: [
'name',
// 返回 `name`
'username DESC',
// 返回 `username DESC`
['username', 'DESC'],
// 返回 `username` DESC
sequelize.fn('max', sequelize.col('age')),
// 返回 max(`age`)
[sequelize.fn('max', sequelize.col('age')), 'DESC'],
// 返回 max(`age`) DESC
[sequelize.fn('otherfunction', sequelize.col('col1'), 12, 'lalala'), 'DESC'],
// 返回 otherfunction(`col1`, 12, 'lalala') DESC
[sequelize.fn('otherfunction', sequelize.fn('awesomefunction', sequelize.col('col'))), 'DESC']
// 返回 otherfunction(awesomefunction(`col`)) DESC, 有可能是无限循环
[{ raw: 'otherfunction(awesomefunction(`col`))' }, 'DESC']
// 也可以这样写
]
})
// 分页查询
Project.findAll({ limit: 10 })
Project.findAll({ offset: 8 })
Project.findAll({ offset: 5, limit: 5 })
//关联查询 include 支持嵌套,这可能是ORM里面最难的部分。
var User = sequelize.define('user', { name: Sequelize.STRING })
, Task = sequelize.define('task', { name: Sequelize.STRING })
, Tool = sequelize.define('tool', { name: Sequelize.STRING })
Task.belongsTo(User) // 增加外键属性 UserId 到 Task
User.hasMany(Task) // 给 Task 增加外键属性 userId
User.hasMany(Tool, { as: 'Instruments' }) // 给 Task 增加自定义外键属性 InstrumentsId
Task.findAll({ include: [ User ] })
User.findAll({ include: [{
model: Tool,
as: 'Instruments',
where: { name: { $like: '%ooth%' } }
}] })
User.findAll({ include: ['Instruments'] })
var User = this.sequelize.define('user', {/* attributes */}, {underscored: true})
, Company = this.sequelize.define('company', {
uuid: {
type: Sequelize.UUID,
primaryKey: true
}
});
User.belongsTo(Company); // 增加 company_uuid 外键属性到 user
User.belongsTo(UserRole, {as: 'role'});
// 自定义外键属性 roleId 到 user 而不是 userRoleId
User.belongsTo(Company, {foreignKey: 'fk_companyname', targetKey: 'name'}); // 增加自定义外键属性 fk_companyname 到 User
Person.hasOne(Person, {as: 'Father', foreignKey: 'DadId'})
// Person 增加外键属性 DadId
Coach.hasOne(Team) // `coachId` 作为 Team 的外键属性
Project.hasMany(User, {as: 'Workers'})
// 给 User 增加外键属性 projectId / project_id
Project.belongsToMany(User, {through: 'UserProject'});
User.belongsToMany(Project, {through: 'UserProject'});
// 创建新的模型: UserProject 包含外键属性:projectId 和 userId
4) Sequelize还有完善的迁移同步数据方案,migrate so easy。1
2
3
4
5
6
7
8
9//$ sequelize db:migrate //用命令直接生成模版脚本,接下来的还是写js
module.exports = {
up: function(queryInterface, Sequelize) {
// 需要修改数据库的操作
},
down: function(queryInterface, Sequelize) {
// 取消修改的操作
}
}
二、知识点
2.1 官网地址
文档地址:https://sequelize.org/v5/index.html
github地址:https://github.com/demopark/sequelize-docs-Zh-CN
更多参考地址:http://www.nodeclass.com/api/sequelize.html
2.2 安装1
2
3
4#安装sequelize包
npm install --save sequelize
#安装mysql包
npm install --save mysql2
2.3 语句映射
sequelize中sql操作函数及mysql语句略有不同,映射如下:
mysql关键字 | sequelize函数关键字 |
---|---|
select | find |
update | update |
insert | create |
delete | destory |
2.4 部分关键参数
参数 | 作用 |
---|---|
raw:true | sql语句执行后,只返回原始数据,没有附加信息 |
freezeTableName: true | 定义model时,默认false(即修改表名为复数),true不修改表名,与数据库表名同步 |
如:modelName: ‘Stu’ | 指定model名,相当于sql语句中表的别名,如select * from stu as Stu; 这里Stu即Model |
如:tableName: ‘stu’, | 指定表名,如select * from stu ; |
三、使用示例
1 | // # 2.1 创建连接sequelize_config.js |
四、扩展
1 | // ### 4.1 sequelize.define定义model |
TypeORM
01:30